<?php

/**
 * This file is part of the Codizer package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @copyright   Copyright (c) 2010 Advisa (http://www.advisa.fr)
 * @author      Codizer Core Team <team@codizer.org>
 * @package     Codizer
 */

class f_package_Manager
{
    private static $instance;

	/**
	 * @return f_package_Manager
	 */
	private function __construct()
	{
		return $this;
	}

	/**
	 * Get singleton instance
	 *
	 * @return f_package_Manager
	 */
	public static function getInstance()
	{
		if(self::$instance === null)
		{
			self::$instance = new self();
		}
		return self::$instance;
	}

    /**
     * Build a package for an application
     * WARNING : You need a package declaration file in webapp/pkg
     *
     * @param string $name
     */
    public function buildApplicationPackage($name)
    {
        $file = WEBAPP_DIR.DS.'pkg'.DS.$name.'.xml';

        if(is_file($file))
        {
            $description = f_package_Description::getInstance($file);

            $components = $description->getComponents();

            if(count($components) > 0)
            {
                $filesList = array();
                
                foreach($components as $component)
                {
                    /* @var $component f_xml_XmlElement */
                    $name = $component->getAttributeValue('name');
                    $version = $component->getAttributeValue('version');
                    $desc = $this->getDescriptionByLabel($name);
                    if(is_null($desc))
                    {
                        if(CLI_MODE) echo COLOR_RED.'ERROR '.COLOR_NORMAL.' -> Package description not found '.$name."\n";
                        return null;
                    }
                    else
                    {
                        if(version_compare($version, $desc->getVersion(), '=='))
                        {
                            if(CLI_MODE) echo COLOR_GREEN.'WRITING '.COLOR_NORMAL.' -> Adding package '.$desc.' '.$desc->getVersion()."\n";
                            $filesList = array_merge($filesList, $this->getDirectoryFiles($desc->getDir()));
                            $filesList = array_merge($filesList, $desc->getExtFiles());
                        }
                        else
                        {
                            if(CLI_MODE) echo COLOR_RED.'ERROR '.COLOR_NORMAL.' -> Package has not the correct version : '.$name.' is '.$desc->getVersion().' (required = '.$version.')'."\n";
                            return null;
                        }
                    }
                }

                if(count($filesList) > 0)
                {
                    $zipFileName = $description->getType().'_'.$description->getName().'-'.$description->getVersion().$description->getStatusLetter().'.zip';
                    $this->packFiles($filesList, $zipFileName);
                }
            }
        }
    }   

    /**
     * Get a package description from a package label
     *
     * @example getDescriptionByLabel('core_module_backoffice')
     * @param string $label
     * @return f_package_Description
     */
    public function getDescriptionByLabel($label)
    {
        list($type, $category, $name) = explode('_', $label);
        return $this->getDescriptionByType($category, $name);
    }

    /**
     * Get the package.xml directory by type and name
     *
     * @param string $type (framework, theme, module, javascript)
     * @param string $name
     * @return string|null
     */
    private function getDirectoryByType($type, $name)
    {
        switch ($type)
        {
            case 'framework':
                $dir = FRAMEWORK_HOME;
            break;

            case 'theme':
                $dir = HTTP_DIR.DS.'themes'.DS.$name;
            break;

            case 'module':
                $dir = MODULES_DIR.DS.$name;
            break;

            case 'javascript':
                $dir = HTTP_DIR.DS.'js';
            break;

            default:
                $dir = null;
            break;
        }

        return $dir;
    }

    /**
     * Get description file from type and name
     * @param string $type
     * @param string $name
     * @return f_package_Description
     */
    public function getDescriptionByType($type, $name)
    {
        $dir = $this->getDirectoryByType($type, $name);

        if(!is_null($dir))
        {
            if($this->hasPackageDescriptionFile($dir))
            {
                return f_package_Description::getInstance($dir.DS.'package.xml');
            }
        }

        return null;
    }

    /**
     * Build a zip file and a xml package description in webapp/pkg/build by type and name
     *
     * @param string $type
     * @param string $name
     */
    private function buildPackageByType($type, $name)
    {
        $description = $this->getDescriptionByType($type, $name);

        if(!is_null($description))
        {
            if($description->getCategory() == $type)
            {
                $files = $this->getDirectoryFiles($description->getDir());
                $files = array_merge($files, $description->getExtFiles());

                $zipFileName = $description.'-'.$description->getVersion().$description->getStatusLetter().'.zip';

                $this->packFiles($files, $zipFileName);
                $this->buildPackageDescription($zipFileName, $description);
            }
            else
            {
                if(CLI_MODE) echo COLOR_RED.'ERROR '.COLOR_NORMAL.' -> Package category is not '.$type."\n";
            }
        }
        else
        {
            $dir = str_replace(BASE_DIR, '', $this->getDirectoryByType($type, $name));

            if(!is_null($dir))
            {
                if(CLI_MODE) echo COLOR_RED.'ERROR '.COLOR_NORMAL.' -> Package file '.$dir.'/package.xml not found !'."\n";
            }
            else
            {
                if(CLI_MODE) echo COLOR_RED.'ERROR '.COLOR_NORMAL.' -> Package file not found !'."\n";
            }
        }
    }

    /**
     * Build a zip file and a xml package description in webapp/pkg/build by label
     *
     * @example buildPackageByLabel('core_module_backoffice')
     * @param string $name
     */
    private function buildPackageByLabel($name)
    {
        list($type, $category, $name) = explode('_', $name);

        $this->buildPackageByType($category, $name);
    }

    /**
     * Build the framework type package
     * WARNING : only for CODIZER !!! ;)
     */
    public function buildFrameworkPackage()
    {
        $this->buildPackageByType('framework', 'codizer');
    }

    /**
     * Build a template type package
     * Templates are in www/themes directory
     *
     * @param string $name
     */
    public function buildTemplatePackage($name)
    {
        $this->buildPackageByType('theme', $name);
    }

    /**
     * Build a module type package
     * Modules are in modules directory
     *
     * @param string $name
     */
    public function buildModulePackage($name)
    {
        $this->buildPackageByType('module', $name);
    }

    /**
     * Build a javascript type package
     * JS are in js/lib directory
     *
     * @param <type> $name
     */
    public function buildJavascriptPackage($name)
    {
        $this->buildPackageByType('javascript', $name);
    }

    /**
     * Build all found packages in application in separated zip and xml files
     */
    public function buildAll()
    {
        $files = f_file_Resolver::getInstance()->fromDirectory(BASE_DIR)->setMask('package.xml')->setRecursive(true)->find();
        if(count($files) > 0)
        {
            foreach($files as $file)
            {
                $description = f_package_Description::getInstance($file);

                if($description != '__')
                {
                    $this->buildPackageByLabel($description);
                }
            }
        }
    }

    /**
     * Check if directory has a package.xml file into
     * @param string $directory
     * @return boolean
     */
    private function hasPackageDescriptionFile($directory)
    {
        return is_file($directory.DS.'package.xml');
    }

    /**
     * Get all files in a directory, excluding svn files
     *
     * @param string $directory
     * @return array
     */
    private function getDirectoryFiles($directory)
    {
        $svnExclude = array('svn', 'entries', 'all-wcprops', 'svn-base', 'dir-prop-base', 'tmp');

        return f_file_Resolver::getAllFiles($directory, $svnExclude);
    }

    /**
     * For CLI, ask to create a package (zip and xml files)
     *
     * @param array $files
     * @param string $zipFileName
     */
    private function packFiles($files, $zipFileName)
    {
        if(CLI_MODE)
        {
            $path = WEBAPP_DIR.DS.'pkg'.DS.'build';

            if(!is_dir($path))
            {
                mkdir($path, 0775, true);
            }

            $admin = new admin;
            if(is_file($path.DS.$zipFileName))
            {
                if($admin->askQuestion('Overwrite file packages/'.$zipFileName.' ?'))
                {
                    echo COLOR_GREEN.'WRITING '.COLOR_NORMAL.' -> Creating package '.$zipFileName.', please wait...'."\n";
                    $this->zipFiles($files, $path.DS.$zipFileName);
                }
            }
            else
            {
                echo COLOR_GREEN.'WRITING '.COLOR_NORMAL.' -> Creating package '.$zipFileName.', please wait...'."\n";
                $this->zipFiles($files, $path.DS.$zipFileName);
            }
        }
    }

    private function zipFiles($files, $zipFilePathName)
    {
        $zip = f_package_Zip::getInstance();

        $dirName = explode(DS, str_replace(BASE_DIR.DS, '', $files[0]));

        foreach($files as $file)
        {
            $fileData = file_get_contents($file);
            $zip->addFile($fileData, str_replace(BASE_DIR.DS, '', $file));
        }

        $zip->save($zipFilePathName);
    }

    /**
     * Create a package description file which is associated to the zip file
     * Used for repository, version control, etc...
     *
     * @param string $zipFile
     * @param f_package_Description $description
     */
    private function buildPackageDescription($zipFile, $description)
    {
        $path = WEBAPP_DIR.DS.'pkg'.DS.'build';
        
        $xml = new XMLWriter();
        $xml->openMemory();
        $xml->setIndent(true);
        $xml->setIndentString('    ');
        $xml->startDocument('1.0', 'UTF-8');
        $xml->startElement('package');
        $xml->writeElement('name', $description);
        $xml->writeElement('version', $description->getVersion().$description->getStatusLetter());
        $xml->writeElement('date', date(DATE_RFC822));
        $xml->writeElement('md5checksum', md5_file($path.DS.$zipFile));
        $xml->writeElement('zipName', $zipFile);

        $xml->endElement();
        $xml->endDocument();
        $content = $xml->outputMemory();

        $xmlFile = str_replace('.zip', '', $zipFile).'.xml';

        $f = fopen($path.DS.$xmlFile, 'w+');

        fwrite($f, $content);

        fclose($f);
    }

// ================================== INSTALL & UPGRADE METHODS ========================

    public function getAllPackagesDescriptions()
    {
        $descriptions = f_file_Resolver::getInstance()->fromDirectory(BASE_DIR)->setRecursive(true)->setMask('package.xml')->find();

        $result = array();

        if(count($descriptions) > 0)
        {
            foreach ($descriptions as $description)
            {
                $result[] = f_package_Description::getInstance($description);
            }
        }

        return $result;
    }

    public function getInstalledPackageVersion($label)
    {
        $dbProvider = f_database_Provider::getInstance();

        $sql = "SELECT `version` FROM `f_packages` WHERE `label`=:label";

        return $dbProvider->setQuery($sql)->execute(array('label' => $label))->getQueryResult(PDO::FETCH_COLUMN);
    }

    public function initOrUpgradePackageByLabel($label)
    {
        $description = $this->getDescriptionByLabel($label);

        if(is_object($description))
        {
            return $this->initOrUpgradePackageFromDescription($description);
        }

        return false;
    }

    private function initOrUpgradePackageFromDescription($description)
    {      
        if(!is_null($description))
        {
            $installedVersion = $this->getInstalledPackageVersion($description->getLabel());

            if(is_null($installedVersion))
            {
                $dbProvider = f_database_Provider::getInstance();
                if(CLI_MODE) echo COLOR_GREEN.'INSTALL'.COLOR_NORMAL.' -> Install module '.$description->getLabel().' version '.$description->getVersion()."\n";
                $this->executeInstallScripts($description);
                $dbProvider->insert('f_packages', array('label' => $description->getLabel(), 'version' => $description->getVersion()));
                return true;
            }
            else
            {                
                $this->upgradePackage($description, $installedVersion);
                return true;
            }
        }
        else
        {
            if(CLI_MODE) echo COLOR_RED.'ERROR  '.COLOR_NORMAL.' -> Module '.$moduleName.' has no package file !'."\n";
        }
        
        return false;
    }

    public function upgradeAllPackages()
    {
        $descriptions = $this->getAllPackagesDescriptions();

        if(count($descriptions) > 0)
        {
            foreach($descriptions as $description)
            {
                /* @var $description f_package_Description */
                $this->initOrUpgradePackageFromDescription($description);
            }
        }
    }

    private function executeInstallScripts($description)
    {
        $rootPath = $description->getDir().DS.'install';

        if(is_dir($rootPath))
        {
            $files = f_file_Resolver::getInstance()->fromDirectory($rootPath)->setMask('*.php')->find();
            foreach($files as $file)
            {
                $installName = str_replace('.class.php', '', str_replace($rootPath.DS, '', $file));
                $className = $description->getLabel().'_'.$installName;

                require_once($file);

                if(class_exists($className))
                {
                    $install = new $className();
                    $install->execute();

                    if(CLI_MODE) echo COLOR_BEIGE.'INSTALL'.COLOR_NORMAL.' -> Execute installation script '.$className."\n";
                }
            }
        }
    }

    private function upgradePackage($description, $currentVersion)
    {
        if(!is_null($description))
        {
            if(version_compare($currentVersion, $description->getVersion(), '<'))
            {
                if(CLI_MODE) echo COLOR_GREEN.'UPGRADE'.COLOR_NORMAL.' -> Upgrade module '.$description->getLabel().' '.$currentVersion.' => '.$description->getVersion()."\n";

                $rootPath = $description->getDir().DS.'patchs';

                if(is_dir($rootPath))
                {
                    $files = f_file_Resolver::getInstance()->fromDirectory($rootPath)->setRecursive(true)->setMask('*.php')->find();

                    if(count($files) > 0)
                    {
                        foreach($files as $file)
                        {
                            list($version, $patch) = explode(DS, str_replace($rootPath.DS, '', $file));

                            if(version_compare($currentVersion, $version, '<') && version_compare($version, $description->getVersion(), '<='))
                            {
                                require_once($file);

                                $vers = str_replace('.', '_', $version);
                                $patchName = str_replace('.class.php', '', $patch);
                                $className = $description->getLabel().'_'.$vers.'_'.$patchName;

                                if(class_exists($className))
                                {
                                    $patch = new $className();
                                    $patch->patch();

                                    if(CLI_MODE) echo COLOR_BEIGE.'PATCH  '.COLOR_NORMAL.' -> Executing patch '.$className."\n";
                                }
                            }
                        }
                    }
                }

                $dbProvider = f_database_Provider::getInstance();
                $sql = 'UPDATE `f_packages` SET `version` = :version WHERE `label` = :label LIMIT 1';
                $dbProvider->setQuery($sql)->execute(array('version' => $description->getVersion(), 'label' => $description->getLabel()));
            }
            else
            {
                if(CLI_MODE) echo COLOR_GREEN.'UPGRADE'.COLOR_NORMAL.' -> Module '.$description->getLabel().' has already the last version installed'."\n";
            }
        }
        else
        {
            if(CLI_MODE) echo COLOR_RED.'ERROR  '.COLOR_NORMAL.' -> Module '.$description->getLabel().' has no package file !'."\n";
        }
    }
}